﻿using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;

namespace DLR04
{
    /// <summary>
    /// This application expands on DLR03 by allowing dynamic methods to be invoked, either from
    /// within the expression itself or directly on the scope object through the indexer.
    /// </summary>
    public class Program
    {
        public static void Main(string[] args)
        {
            var scope = new Scope();
            var simple = new Simple();

            scope["n1"] = 5;
            scope["n2"] = 7;
            simple.Run(scope);

            Console.WriteLine("Scope dump:");
            foreach (var item in scope)
            {
                Console.WriteLine("{0} = {1}", item.Name, item.Data);
            }
            Console.WriteLine();

            Console.WriteLine("Results of dynamic add call: {0}", scope["add"](13, 24));
        }
    }

    public class Simple
    {
        private Dictionary<string, object> _symbols;

        private ParameterExpression _runtime;
        private ParameterExpression _scope;

        public Simple()
        {
            _symbols = new Dictionary<string, object>();
            _runtime = Expression.Parameter(typeof(Simple), "runtime");
            _scope = Expression.Parameter(typeof(Scope), "scope");
        }

        public object Run(Scope scope)
        {
            var n1 = Expression.Parameter(typeof(object), "n1");
            var n2 = Expression.Parameter(typeof(object), "n2");

            var addBody = new List<Expression>()
            {
                Expression.Assign(
                    ExpressionEx.SymbolExpression(_scope, "add"),
                    Expression.Lambda(ExpressionEx.DynamicAdd(n1, n2), n1, n2)
                ),
                Expression.Assign(
                    ExpressionEx.SymbolExpression(_scope, "result"),
                    ExpressionEx.DynamicInvoke(
                        ExpressionEx.SymbolExpression(_scope, "add"),
                        ExpressionEx.SymbolExpression(_scope, "n1"),
                        ExpressionEx.SymbolExpression(_scope, "n2")
                    )
                )
            };

            var function = Expression.Lambda<Func<Simple, Scope, object>>(
                Expression.Block(addBody),
                _runtime, _scope);

            var compiled = function.Compile();

            return compiled(this, scope);
        }
    }

    /// <summary>
    /// This is the minimum required to invoke a dynamic method.
    /// </summary>
    /// <remarks>
    /// This is by no means as safe as it should be.  It needs to be cleaned up.
    /// </remarks>
    public class SimpleInvokeBinder : InvokeBinder
    {
        public SimpleInvokeBinder(CallInfo callInfo)
            : base(callInfo)
        {
        }

        public override DynamicMetaObject FallbackInvoke(DynamicMetaObject target, DynamicMetaObject[] args, DynamicMetaObject errorSuggestion)
        {
            if (target.LimitType.IsSubclassOf(typeof(Delegate)))
            {
                var parms = target.LimitType.GetMethod("Invoke").GetParameters();
                var callArgs = ExpressionEx.ConvertArguments(args, parms);
                return new DynamicMetaObject(
                    ExpressionEx.EnsureObjectResult(
                        Expression.Invoke(Expression.Convert(target.Expression, target.LimitType), callArgs)),
                    BindingRestrictions.GetTypeRestriction(target.Expression, target.LimitType));
            }

            return new DynamicMetaObject(
                Expression.Throw(
                    Expression.New(
                        typeof(Exception).GetConstructor(new[] { typeof(string) }),
                        Expression.Constant("Unable to call the function."))),
                BindingRestrictions.GetTypeRestriction(target.Expression, target.LimitType));
        }
    }

    public static class ExpressionEx
    {
        /// <summary>
        /// Returns list of Convert expressions converting arguments to parameter types.
        /// </summary>
        public static Expression[] ConvertArguments(DynamicMetaObject[] args, ParameterInfo[] parms)
        {
            Expression[] callArgs = new Expression[args.Length];
            for (int index = 0; index < args.Length; index++)
            {
                callArgs[index] = Expression.Convert(args[index].Expression, parms[index].ParameterType);
            }
            return callArgs;
        }

        public static Expression DynamicInvoke(Expression expr, params Expression[] args)
        {
            List<Expression> finalArgs = new List<Expression>(args);
            finalArgs.Insert(0, expr);
            return Expression.Dynamic(
                new SimpleInvokeBinder(new CallInfo(args.Length)),
                typeof(object),
                finalArgs);
        }

        /// <summary>
        /// Add two dynamic values together.
        /// </summary>
        public static Expression DynamicAdd(Expression left, Expression right)
        {
            return Expression.Dynamic(
                        new SimpleBinaryOperationBinder(ExpressionType.Add),
                        typeof(object),
                        left,
                        right);
        }

        /// <summary>
        /// Retrieve a variable from the input scope by name.
        /// </summary>
        /// <param name="scope">An expression which refers to an object of type Scope.</param>
        /// <param name="symbolName">A symbol name within the given Scope object.</param>
        public static Expression SymbolExpression(Expression scope, string symbolName)
        {
            if (scope.Type != typeof(Scope))
            {
                throw new ArgumentException("The scope expression must represent a Scope object.");
            }
            return Expression.Property(Expression.Call(scope, "GetSymbol", null, Expression.Constant(symbolName)), "Data");
        }

        /// <summary>
        /// Ensure that some sort of object type is returned by the expression.
        /// </summary>
        public static Expression EnsureObjectResult(Expression expr)
        {
            if (!expr.Type.IsValueType)
            {
                return expr;
            }
            if (expr.Type == typeof(void))
            {
                return Expression.Block(expr, Expression.Default(typeof(object)));
            }
            else
            {
                return Expression.Convert(expr, typeof(object));
            }
        }
    }
}